# 面经手册 · 第38篇《MyBatis 一对一、一对多怎么查?延迟加载原理和 N+1 问题怎么解?》

作者:小傅哥
博客:https://bugstack.cn (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

# 一、前言

实际业务中,数据表之间的关系几乎不可能是一张表独立存在的。用户有订单、订单有商品、商品有分类……关联查询是日常开发的高频操作。

MyBatis 中 association 和 collection 是处理关联查询的两大标签,延迟加载是解决性能问题的关键配置,N+1 问题更是面试必问的经典坑。

# 二、面试题

谢飞机,小记!,面试继续。

面试官:MyBatis 一对一关联查询怎么配置?

谢飞机:用 association 标签。

面试官:嵌套查询和嵌套结果有什么区别?

谢飞机:嵌套查询是两条 SQL,嵌套结果是一条 SQL?

面试官:对,那嵌套查询有什么性能问题?

谢飞机:N+1?

面试官:怎么解决 N+1?

谢飞机:用延迟加载?

面试官:延迟加载原理是什么?

谢飞机:代理?动态代理?

面试官:什么代理?JDK 还是 CGLIB?代理对象是怎么创建的?

谢飞机:我……再见!ヾ( ̄▽ ̄)

# 三、一对一关联 — association

# 1. 数据模型

user(用户表)          id_card(身份证表)
┌──────┐              ┌──────────────┐
│ id   │              │ id           │
│ name │    ──── 1:1 ─│ user_id      │
│ age  │              │ card_no      │
│      │              │ address      │
└──────┘              └──────────────┘
1
2
3
4
5
6
7

# 2. 嵌套结果(推荐,一条 SQL)

<resultMap id="userWithCardMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <!-- 一对一关联 -->
    <association property="idCard" javaType="com.example.entity.IdCard">
        <id column="card_id" property="id"/>
        <result column="card_no" property="cardNo"/>
        <result column="card_address" property="address"/>
    </association>
</resultMap>

<select id="findUserWithCard" resultMap="userWithCardMap">
    SELECT 
        u.id, u.name, u.age,
        c.id AS card_id, c.card_no, c.address AS card_address
    FROM user u
    LEFT JOIN id_card c ON u.id = c.user_id
    WHERE u.id = #{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 3. 嵌套查询(两条 SQL,需延迟加载配合)

<resultMap id="userLazyMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- 延迟加载关联查询 -->
    <association property="idCard" 
                 javaType="com.example.entity.IdCard"
                 select="com.example.mapper.IdCardMapper.findByUserId"
                 column="id"
                 fetchType="lazy"/>
</resultMap>

<select id="findById" resultMap="userLazyMap">
    SELECT id, name, age FROM user WHERE id = #{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- IdCardMapper.xml -->
<select id="findByUserId" resultType="com.example.entity.IdCard">
    SELECT id, card_no, address FROM id_card WHERE user_id = #{userId}
</select>
1
2
3
4

# 四、一对多关联 — collection

# 1. 数据模型

user(用户表)          orders(订单表)
┌──────┐              ┌──────────────┐
│ id   │              │ id           │
│ name │    ── 1:N ── │ user_id      │
│ age  │              │ order_no     │
│      │              │ amount       │
└──────┘              └──────────────┘
1
2
3
4
5
6
7

# 2. 嵌套结果(一条 SQL)

<resultMap id="userWithOrdersMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <!-- 一对多关联 -->
    <collection property="orders" ofType="com.example.entity.Order">
        <id column="order_id" property="id"/>
        <result column="order_no" property="orderNo"/>
        <result column="amount" property="amount"/>
    </collection>
</resultMap>

<select id="findUserWithOrders" resultMap="userWithOrdersMap">
    SELECT 
        u.id, u.name,
        o.id AS order_id, o.order_no, o.amount
    FROM user u
    LEFT JOIN orders o ON u.id = o.user_id
    WHERE u.id = #{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

注意ofType 指定集合中元素的类型,不是 javaType

# 3. 嵌套查询(两条 SQL)

<resultMap id="userLazyOrdersMap" type="com.example.entity.User">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <collection property="orders" 
                ofType="com.example.entity.Order"
                select="com.example.mapper.OrderMapper.findByUserId"
                column="id"
                fetchType="lazy"/>
</resultMap>
1
2
3
4
5
6
7
8
9

# 五、延迟加载原理

# 1. 配置方式

<!-- mybatis-config.xml -->
<settings>
    <!-- 全局延迟加载开关 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 按需加载(false=访问关联属性时才加载,true=加载主对象后立即加载所有关联) -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>
1
2
3
4
5
6
7

# 2. 局部配置覆盖全局

<!-- 强制立即加载 -->
<association property="idCard" fetchType="eager" .../>

<!-- 强制延迟加载(即使全局关闭了延迟加载) -->
<association property="idCard" fetchType="lazy" .../>
1
2
3
4
5

# 3. 底层原理:代理对象

延迟加载的核心:返回的不是目标对象本身,而是一个代理对象

正常加载(eager):
  查 user → 执行 SQL → 获取 user 对象(真实对象)

延迟加载(lazy):
  查 user → 执行 SQL → 获取 user 代理对象
  ↓ 访问 user.getIdCard() 时
  触发代理拦截 → 执行关联 SQL → 获取 idCard → 注入到 user 对象
  ↓ 再次访问
  直接返回已加载的 idCard(不再执行 SQL)
1
2
3
4
5
6
7
8
9

# 4. 源码追踪

代理创建

// org.apache.ibatis.executor.resultset.DefaultResultSetHandler
private Object createResultObject(ResultSetWrapper rsw, 
        ResultMap resultMap, ...) {
    // 检查是否需要延迟加载
    if (hasNestedResultMaps(rsw, resultMap)) {
        // 创建代理对象
        return createProxyObject(resultMap, ...);
    }
    return objectFactory.create(resultType);
}

// 代理工厂(CGLIB 或 Javassist)
private Object createProxyObject(ResultMap resultMap, ...) {
    return proxyFactory.createProxy(resultObject, 
        new ResultLoaderMap(), lazyLoader, configuration, ...);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

代理拦截

// org.apache.ibatis.executor.loader.javassist.JavassistProxyFactory
public class JavassistProxyFactory implements ProxyFactory {
    
    @Override
    public Object createProxy(Object target, ...) {
        return EnhancedResultObjectProxyImpl.createProxy(target, ...);
    }
}

// 代理拦截逻辑
public static class EnhancedResultObjectProxyImpl implements MethodHandler {
    @Override
    public Object invoke(Object enhanced, Method method, 
            Method proxyMethod, Object[] args) {
        String methodName = method.getName();
        Object value;
        
        // 检查是否是关联属性的 getter 方法
        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
            // 触发延迟加载
            value = lazyLoader.load(finalizeMethod);
        }
        
        // 加载完成后调用原始方法
        return proxyMethod.invoke(target, args);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 5. 代理实现选择

<!-- mybatis-config.xml -->
<settings>
    <!-- CGLIB 或 JAVASSIST -->
    <setting name="proxyFactory" value="JAVASSIST"/>
</settings>
1
2
3
4
5
实现 特点
JAVASSIST(默认) 字节码生成,启动快,运行稍慢
CGLIB 字节码生成,功能强大,需要额外依赖

# 六、N+1 问题

# 1. 什么是 N+1

场景:查询 10 个用户,每个用户关联一张身份证

嵌套查询模式(N+1):
  第 1 条 SQL:SELECT * FROM user(查出 10 个用户)
  第 2 条 SQL:SELECT * FROM id_card WHERE user_id = 1
  第 3 条 SQL:SELECT * FROM id_card WHERE user_id = 2
  ...
  第 11 条 SQL:SELECT * FROM id_card WHERE user_id = 10
  
  总共 11 条 SQL = 1 + 10 = N+1
1
2
3
4
5
6
7
8
9
10

# 2. 解决方案

方案一:嵌套结果(JOIN)

<!-- 一条 SQL 搞定,推荐 -->
<select id="findUserWithCard" resultMap="userWithCardMap">
    SELECT u.*, c.* FROM user u 
    LEFT JOIN id_card c ON u.id = c.user_id
</select>
1
2
3
4
5

方案二:延迟加载

<!-- 开启延迟加载后,只有真正访问关联属性时才执行 SQL -->
<setting name="lazyLoadingEnabled" value="true"/>
1
2

方案三:批量查询(fetchSize)

<association property="idCard" 
             select="com.example.mapper.IdCardMapper.batchFindByUserIds"
             column="id"
             fetchType="lazy"/>
1
2
3
4
<!-- 使用 IN 批量查询 -->
<select id="batchFindByUserIds" resultType="com.example.entity.IdCard">
    SELECT * FROM id_card WHERE user_id IN
    <foreach collection="list" item="userId" open="(" separator="," close=")">
        #{userId}
    </foreach>
</select>
1
2
3
4
5
6
7

方案四:二级缓存

<!-- 第一次查询后缓存,后续命中缓存 -->
<cache/>
1
2

# 3. 各方案对比

方案 SQL 数量 适用场景 注意事项
JOIN 嵌套结果 1 条 关联数据确定需要 JOIN 数据量大时性能下降
延迟加载 按需 关联数据不一定用到 仍然可能 N+1
批量 fetchSize 1 + 1 = 2 嵌套查询 需配置 batch
二级缓存 首次 N+1,后续 0 数据变更少 缓存一致性问题

# 七、常见面试追问

# Q1:association 和 collection 能嵌套使用吗?

能。可以在 collection 中嵌套 association,实现三层甚至更深的关联。但层级越深,SQL 复杂度和性能影响越大。

# Q2:延迟加载对序列化有影响吗?

有。代理对象序列化时可能丢失关联数据。解决:在序列化前先访问关联属性触发加载,或配置 serialization 代理工厂。

# Q3:fetchType 优先级?

局部 fetchType 优先级高于全局 lazyLoadingEnabled 配置。fetchType="eager" 强制立即加载,fetchType="lazy" 强制延迟加载。

# 八、总结

记住三个核心要点:

1. 关联查询两种方式
   嵌套结果(JOIN 一条 SQL):推荐,性能好
   嵌套查询(两条 SQL):灵活,但可能 N+1

2. 延迟加载原理
   返回代理对象 → 访问关联属性时拦截 → 执行关联 SQL
   通过 Javassist/CGLIB 创建代理,invoke() 触发加载

3. N+1 问题解决
   优先用 JOIN 嵌套结果(1条SQL)
   或延迟加载 + 批量 fetchSize(2条SQL)
   二级缓存可作为辅助
1
2
3
4
5
6
7
8
9
10
11
12
13
14

面试回答模板

MyBatis 通过 association 和 collection 标签处理一对一和一对多关联查询。两种实现方式:嵌套结果用 JOIN 一条 SQL 查完,推荐使用;嵌套查询分两条 SQL 执行,灵活但可能产生 N+1 问题。

延迟加载的原理是返回代理对象而非真实对象,通过 Javassist 或 CGLIB 创建代理,拦截 getter 方法,在访问关联属性时才触发关联 SQL 执行。配置上全局开启 lazyLoadingEnabled=true,局部可通过 fetchType 覆盖。

N+1 问题的最优解是嵌套结果用 JOIN 查询,如果必须用嵌套查询,则配合延迟加载和批量 fetchSize 来减少 SQL 次数。